﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Globalization;
using Engine.Sgml.Decoding;
using System.Diagnostics;
using Engine.Sgml.Parser;

namespace Engine.Sgml {
    /// <summary>
    /// This class decodes an HTML/XML stream correctly.
    /// </summary>
    internal class SgmlStream : TextReader {
        private Stream stream;
        private byte[] byteBuffer;
        private char[] charBuffer;

        private int readBytesCount;
        private int readCharsCount;
        private int position;

        private Decoder decoder;
        /// <summary>
        /// Average web page is 320K
        /// </summary>
        public const int BufferSize = 16384;
        public const int EndOfFile = -1;

        internal struct Memento {
            private int position;
            private int streamPosition;
            private SgmlStream stream;

            internal Memento(SgmlStream stream) {
                Guard.ArgumentNotNull(stream, "stream");
                this.stream = stream;
                streamPosition = Convert.ToInt32(stream.stream.Position) - stream.readBytesCount;
                position = stream.position;
                }

            internal void Restore() {
                Debug.Assert(stream.stream.CanSeek);
                stream.stream.Position = streamPosition;
                stream.position = stream.readCharsCount = 0;
                stream.ReadAndDecodeNextBlock();
                stream.position = position;
                }
            }

        internal Encoding DetectEncoding(Context context, SElement root) {
            Debug.Assert(context != null);
            Debug.Assert(context.Readers.Count > 0);
            // Maximum byte order mark
            readBytesCount = stream.Read(byteBuffer, 0, 4);
            int byteBufferPosition = 0;
            // Check byte order marks
            decoder = Detector.DetectDecoderFromOrderByteMarks(byteBuffer, ref byteBufferPosition, readBytesCount);
            if (decoder == null) {
                decoder = Encoding.GetDecoder();
                readBytesCount += stream.Read(byteBuffer, 4, byteBuffer.Length - 4);
                DecodeBlock(ref position, ref readCharsCount, readBytesCount, byteBufferPosition);

                // Save origin stream
                int rbc = readBytesCount;
                Stream originStream = stream;
                stream = new MemoryStream(byteBuffer);
                SgmlStreamReader reader = new SgmlStreamReader(this);
                // Disable tracing
                SourceLevels sourceLevel = context.TraceSource.Switch.Level;
                context.TraceSource.Switch.Level = SourceLevels.Off;

                // Now sniff to see if there is an XML declaration or HTML <META> tag.
                Encoding encoding = Detector.DetectEncodingFromXmlDeclaration(reader, root, context);
                if (encoding == null)
                    encoding = Detector.DetectEncodingFromMetaTag(reader, root, context);
                if (encoding != null)
                    Encoding = encoding;

                decoder = Encoding.GetDecoder();
                // Restore the state of trace source
                context.TraceSource.Switch.Level = sourceLevel;
                // Restore the origin stream
                stream.Close();
                stream = originStream;
                readBytesCount = rbc;
                position = byteBufferPosition = readCharsCount = 0;
                DecodeBlock(ref position, ref readCharsCount, readBytesCount, byteBufferPosition);
                }
            else if (byteBufferPosition > 0)
                DecodeBlock(ref position, ref readCharsCount, readBytesCount, byteBufferPosition);
            return Encoding;
            }

        public SgmlStream(Stream stream, Encoding encoding, int bufferSize = BufferSize)
            : base() {
            Debug.Assert(stream != null);
            Debug.Assert(stream.CanRead);
            Guard.ArgumentNotNull(stream, "stream");
            Guard.ArgumentNotNull(encoding, "encoding");
            if (bufferSize < 4)
                throw new ArgumentException("The buffer size must be great than 4.");
            this.Encoding = encoding;
            this.decoder = Encoding.GetDecoder();
            this.stream = stream;
            this.byteBuffer = new byte[bufferSize];
            this.charBuffer = new char[bufferSize];
            }

        private void DecodeBlock(ref int position, ref int readCharsCount, int readBytesCount, int byteBufferPosition) {
            Debug.Assert(position == readCharsCount);
            position = readCharsCount = 0;
            int charCount = decoder.GetCharCount(byteBuffer, byteBufferPosition, readBytesCount - byteBufferPosition);
            if (charBuffer.Length < charCount)
                charBuffer = new char[charCount];
            readCharsCount = decoder.GetChars(byteBuffer, byteBufferPosition, readBytesCount - byteBufferPosition, charBuffer, position);
            }

        /// <summary>
        /// Gets the auto detected encoding that is used to convert an encoded sequence of bytes into a sequence of characters
        /// or when the auto detect feature was unable to detect the character encoding, returns the default encoding.
        /// </summary>
        public Encoding Encoding { get; private set; }

        public override int Peek() {
            if (position < readCharsCount)
                return charBuffer[position];
            else if (0 == ReadAndDecodeNextBlock())
                return EndOfFile;
            else
                return charBuffer[position];
            }

        public override int Read() {
            if (position < readCharsCount)
                return charBuffer[position++];
            else if (0 == ReadAndDecodeNextBlock())
                return EndOfFile;
            else
                return charBuffer[position++];
            }

        public override int Read(char[] buffer, int start, int length) {
            if (position == readCharsCount && 0 == ReadAndDecodeNextBlock())
                return EndOfFile;
            if (position < readCharsCount) {
                length = Math.Min(readCharsCount - position, length);
                Array.Copy(this.charBuffer, position, buffer, start, length);
                position += length;
                return length;
                }
            return 0;
            }

        private int ReadAndDecodeNextBlock() {
            readBytesCount = stream.Read(byteBuffer, 0, byteBuffer.Length);
            if (readBytesCount == 0)
                return 0;
            DecodeBlock(ref position, ref readCharsCount, readBytesCount, 0);
            return readBytesCount;
            }

        public override int ReadBlock(char[] data, int index, int count) {
            return Read(data, index, count);
            }

        public override string ReadToEnd() {
            // Large block heap is more efficient
            if (Peek() == EndOfFile)
                return string.Empty;
            StringBuilder builder = new StringBuilder();
            while (position < readCharsCount) {
                builder.Append(charBuffer, 0, readCharsCount);
                position = readCharsCount;
                if (0 == ReadAndDecodeNextBlock())
                    break;
                }
            return builder.ToString();
            }

        protected override void Dispose(bool disposing) {
            if (disposing) {
                if (stream != null)
                    stream.Dispose();
                }
            base.Dispose(disposing);
            }

        private static Stream CopyToMemory(Stream stream, int capacity) {
            if (!stream.CanSeek)
                return stream;
            MemoryStream memoryStream = new MemoryStream(capacity > 0 ? capacity : 0);
            byte[] buffer = new byte[84987];
            Debug.Assert(GC.GetGeneration(buffer) != 2, "The short-life object is too large.");
            try {
                int length = 0;
                while ((length = stream.Read(buffer, 0, buffer.Length)) > 0)
                    memoryStream.Write(buffer, 0, length);
                memoryStream.Seek(0, SeekOrigin.Begin);
                }
            finally {
                stream.Close();
                }
            return memoryStream;
            }
        }
    }
